package com.zelic.znn.core;

/**
 * @since 17/07/2011 
 * @author zelic
 * Filename: Network.java
 * Description: Infrastructure of ANN
 */
import com.zelic.znn.functions.OutputFunction;
import java.io.*;

/**
 * Network is used to manage all layers
 * @author zelic
 */
public class Network {

    protected Layer first = null;
    protected Layer last = null;
    protected int numberLayers = 0;

    /**
     * Constructor for network
     * (Empty)
     */
    public Network() {
    }

    /**
     * Add a layer to network
     * @param layer : layer need to be added
     */
    public void addLayer(Layer layer) {
        numberLayers++;

        if (first == null) {
            first = last = layer;
        } else {
            layer.setPreviousLayer(last);
            last.setNextLayer(layer);
            last = layer;
        }
    }

    /**
     * Add new layer to network
     * @param numInput : number of elements of input 
     * @param numUnit : number of elements of unit of layer
     * @param rate : learning rate
     */
    public void addLayer(int numInput, int numUnit, double rate) {
        Layer n = new Layer(numInput, numUnit, this, rate);
        numberLayers++;

        if (first == null) {
            first = last = n;
        } else {
            n.setPreviousLayer(last);
            last.setNextLayer(n);
            last = n;
        }
    }

    /**
     * Add new layer to network with specific output method
     * @param numInput: number of elements of input
     * @param numUnit : number of elements of unit
     * @param rate : learning rate
     * @param outputMethod : output method (Sigmoid, BipolarSigmoid, Tanh)
     */
    public void addLayer(int numInput, int numUnit, double rate, OutputFunction outputMethod) {
        Layer n = new Layer(numInput, numUnit, this, rate, outputMethod);
        numberLayers++;

        if (first == null) {
            first = last = n;
        } else {
            n.setPreviousLayer(last);
            last.setNextLayer(n);
            last = n;
        }
    }

    /**
     * Calculate error for specific training example follow formula:
     * Error = 1/2*sumOf(output-target)^2 for each example
     * @param trainingExample : A training example
     * @return error in double
     */
    protected double calculateError(TrainingExample trainingExample) {
        double error = 0;
        double[] target = trainingExample.getOutput();
        double[] output = last.getOutput();
        int numOutput = target.length;

        for (int i = 0; i < numOutput; i++) {
            error = error + 0.5 * Math.pow(output[i] - target[i], 2);
        }

        return error;
    }

//    public double trainWithOneEpoch(TrainingSet trainingSet) {
//        int numExample = trainingSet.getNumTrainingData();
//        int numInputElement = trainingSet.getNumInput();
//        int numTargetElement = trainingSet.getNumOutput();
//        double error = 0;
//        Layer pointer;
//        double[] input;
//        double[] target;
//        error = 0;
//        for (int j = 0; j < numExample; j++) {
//
//            pointer = first;
//            input = trainingSet.getTrainingExample(j).getInput();
//
//            //Calculate output for each layer
//            while (pointer != null) {
//                pointer.setInput(input);
//                pointer.calculateOutput();
//                input = pointer.getOutput();
//                pointer = pointer.getNextLayer();
//            }
//
//            pointer = last;
//            target = trainingSet.getTrainingExample(j).getOutput();
//
//            //Calculate delta for each layer
//            while (pointer != null) {
//                pointer.calculateDelta(target);
//                pointer = pointer.getPreviousLayer();
//            }
//
//            //Update weight for each layer
//            pointer = first;
//            while (pointer != null) {
//                pointer.updateWeight();
//                pointer = pointer.getNextLayer();
//            }
//            error = error + calculateError(trainingSet.getTrainingExample(j));
//        } //j
//        return error/(2*numExample);
//    }
//
//    /**
//     * Train the network with limited epochs
//     * @param epochs: number of epochs we plan to train
//     * @param trainingSet: training set 
//     * @return overall error
//     */
//    public double trainWithLimitedEpochs(int epochs, TrainingSet trainingSet) {
//        int numExample = trainingSet.getNumTrainingData();
//        int numInputElement = trainingSet.getNumInput();
//        int numTargetElement = trainingSet.getNumOutput();
//        double error = 0;
//        Layer pointer;
//        double[] input;
//        double[] target;
//        for (int i = 0; i < epochs; i++) {
//            error = 0;
//            for (int j = 0; j < numExample; j++) {
//
//                pointer = first;
//                input = trainingSet.getTrainingExample(j).getInput();
//
//                //Calculate output for each layer
//                while (pointer != null) {
//                    pointer.setInput(input);
//                    pointer.calculateOutput();
//                    input = pointer.getOutput();
//                    pointer = pointer.getNextLayer();
//                }
//
//                pointer = last;
//                target = trainingSet.getTrainingExample(j).getOutput();
//
//                //Calculate delta for each layer
//                while (pointer != null) {
//                    pointer.calculateDelta(target);
//                    pointer = pointer.getPreviousLayer();
//                }
//
//                //Update weight for each layer
//                pointer = first;
//                while (pointer != null) {
//                    pointer.updateWeight();
//                    pointer = pointer.getNextLayer();
//                }
//                error = error + calculateError(trainingSet.getTrainingExample(j));
//            } //j
//
//        } //i
//        return error;
//
//    }
//
//    /**
//     * Train the network with limited error
//     * @param targetError : minimum error we want to get
//     * @param trainingSet : training set
//     * @return overall epochs
//     */
//    public int trainWithLimitedError(double targetError, TrainingSet trainingSet) {
//        double error = Double.MAX_VALUE;
//        int numExample = trainingSet.getNumTrainingData();
//        int numInputElement = trainingSet.getNumInput();
//        int numTargetElement = trainingSet.getNumOutput();
//        Layer pointer;
//        double[] input;
//        double[] target;
//        //Count the number of epochs
//        int counter = 0;
//
//        while (error > targetError) {
//            error = 0;
//            counter++;
//            for (int j = 0; j < numExample; j++) {
//
//                pointer = first;
//                input = trainingSet.getTrainingExample(j).getInput();
//
//                //Calculate output for each layer
//                while (pointer != null) {
//                    pointer.setInput(input);
//                    pointer.calculateOutput();
//                    input = pointer.getOutput();
//                    pointer = pointer.getNextLayer();
//                }
//
//                pointer = last;
//                target = trainingSet.getTrainingExample(j).getOutput();
//
//                //Calculate delta for each layer
//                while (pointer != null) {
//                    pointer.calculateDelta(target);
//                    pointer = pointer.getPreviousLayer();
//                }
//
//                //Update weight for each layer
//                pointer = first;
//                while (pointer != null) {
//                    pointer.updateWeight();
//                    pointer = pointer.getNextLayer();
//                }
//                double temp_error = calculateError(trainingSet.getTrainingExample(j));
//                //System.out.print("Epochs: "+counter+" - Current Error: "+temp_error+"\n");
//                error = error + temp_error;
//            }
//            System.out.print("Epochs: " + counter + " - Error: " + error + "\n");
//        }
//        //This can be removed
//        return counter;
//    }

    /**
     * Use the network to calculate in practical
     * @param inputData : input data
     * @return output in double[]
     */
    public double[] use(double[] inputData) {
        double[] result;
        double[] input = inputData;
        Layer pointer = first;
        //Calculate output for each layer
        while (pointer != null) {
            pointer.setInput(input);
            pointer.calculateOutput();
            input = pointer.getOutput();
            pointer = pointer.getNextLayer();
        }
        result = Util.winnerArray(last.getOutput());
        return result;
    }
    
    public double test(TrainingSet trainingSet){
        double error = 0;
        int numExample = trainingSet.getNumTrainingData();
        int numInputElement = trainingSet.getNumInput();
        int numTargetElement = trainingSet.getNumOutput();
        
        for (int i=0;i<numExample;i++){
            double[] result = use(trainingSet.getTrainingExample(i).getInput());
            double[] target = trainingSet.getTrainingExample(i).getOutput();
            for (int j = 0;j<numTargetElement;j++){
                if(Math.abs(target[j]-result[j])>0){
                    error++;
                }
            }
        }
        
        return error*100/numExample;
    }

    /**
     * Import network data from external file
     * @param filename : data file
     * @return a network
     * @throws IOException 
     */
    public static Network importNetworkFromFile(String filename) throws IOException {
        Network result = new Network();
        DataInputStream inputStream = null;
        try {
            double[][] weight = null;
            inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(filename)));
            int numLayer = inputStream.readInt();
            for (int l = 0; l < numLayer; l++) {
                int numInput = inputStream.readInt();
                int numUnit = inputStream.readInt();
                double learningRate = inputStream.readDouble();

                weight = new double[numUnit][numInput + 1];

                for (int i = 0; i < numUnit; i++) {
                    for (int j = 0; j <= numInput; j++) {
                        weight[i][j] = inputStream.readDouble();
                    }
                }
                Layer temp = new Layer(numInput, numUnit, result, learningRate);
                temp.setWeight(weight);
                result.addLayer(temp);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }

        return result;
    }
    
    /**
     * Import network data from external file
     * @param input : BufferedInputStream
     * @return a network
     * @throws IOException 
     */
    public static Network importNetworkFromFile(BufferedInputStream input) throws IOException {
        Network result = new Network();
        DataInputStream inputStream = null;
        try {
            double[][] weight = null;
            inputStream = new DataInputStream(input);
            int numLayer = inputStream.readInt();
            for (int l = 0; l < numLayer; l++) {
                int numInput = inputStream.readInt();
                int numUnit = inputStream.readInt();
                double learningRate = inputStream.readDouble();

                weight = new double[numUnit][numInput + 1];

                for (int i = 0; i < numUnit; i++) {
                    for (int j = 0; j <= numInput; j++) {
                        weight[i][j] = inputStream.readDouble();
                    }
                }
                Layer temp = new Layer(numInput, numUnit, result, learningRate);
                temp.setWeight(weight);
                result.addLayer(temp);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }

        return result;
    }

    /**
     * Export current network to external file
     * @param filename : data file
     * @throws IOException 
     */
    public void exportNetworkToFile(String filename) throws IOException {
        DataOutputStream outputStream = null;
        try {
            outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));
            outputStream.writeInt(this.numberLayers);

            Layer temp = first;
            while (temp != null) {
                int numInput = temp.getNumInput();
                int numUnit = temp.getNumUnit();
                outputStream.writeInt(numInput - 1);
                outputStream.writeInt(numUnit);
                outputStream.writeDouble(temp.getLearningRate());
                double[][] weight = temp.getWeight();
                for (int i = 0; i < numUnit; i++) {
                    for (int j = 0; j < numInput; j++) {
                        outputStream.writeDouble(weight[i][j]);
                    }
                }
                temp = temp.getNextLayer();
            }
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }

    }
}
